/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package meteocal.control;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.logging.Logger;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.inject.Named;
import meteocal.entity.AlternativeNotification;
import meteocal.entity.Event;
import meteocal.entity.Notification;
import meteocal.entity.Users;
import meteocal.entity.WeatherNotification;
import meteocal.entityManager.EventManager;
import meteocal.entityManager.NotificationManager;
import meteocal.entityManager.UserManager;
import meteocal.exception.CityNotFoundException;
import meteocal.weather.ForecastProvider;
import meteocal.exception.UnavailableForecastException;
import meteocal.weather.Weather;

/**
 *
 * @author andrea
 */
@Named(value = "weather controller")
@Stateless
public class WeatherControl implements Serializable{
    
    @Inject
    EventManager em;
    
    @Inject
    NotificationManager nm;
    
    @Inject
    NotificationControl nc;
    
    @Inject
    UserManager um;
    
    @Inject
    ForecastProvider fp;
    
    /**
     * Retrieves weather forecast for a specified event and notifies the creator
     * and the participating users in case of bad weather. It also suggests to the
     * creator an alternative date, if available, with better weather conditions.
     * @param event
     * @return weather forecast
     * @throws CityNotFoundException if the specified event place is not supported
     * @throws UnavailableForecastException if there are no forecasts for the event date.
     */
    public Weather getEventForecast(Event event) throws UnavailableForecastException, CityNotFoundException{
        Weather weather = null;
        String place = event.getPlace();
        Calendar date = event.getStartTime();
        try {
            //Asks for weather forecast
            weather = fp.getForecast(place, date);
            Logger.getLogger(WeatherControl.class.getName())
                    .info(String.format("Requested forecasts for %s: %s", place, weather.toString().toLowerCase()));
            //Checks if forecasts are bad for the event
            if(weather.isWorseThan(event.getBadWeather())){
                //Tries to find a date with better weather condition
                Calendar newDate = fp.findAlternative(event);
                if(newDate == null){
                    
                    WeatherNotification not = buildNotification(event, weather);
                    if(isNotifiable(event.getCreator(), not)){
                        List<Users> user = new ArrayList<>();
                        user.add(event.getCreator());
                        nc.sendNotification(not, user);
                    }
                }else{
                //Notifies the creator of the event
                AlternativeNotification alt = buildAlternative(event, weather, newDate);
                if(isNotifiable(event.getCreator(), alt)){
                    List<Users> user = new ArrayList<>();
                    user.add(event.getCreator());
                    nc.sendNotification(alt, user);
                    }
                }
                //Notifies participants
                WeatherNotification not = buildNotification(event, weather);
                List<Users> users = new ArrayList<>();
                for(Users u : em.getParticipants(event)){
                    if(isNotifiable(u, not))
                        users.add(u);
                }
                nc.sendNotification(not, users);
                
            }
        }
        catch (UnavailableForecastException ex) {
            Logger.getLogger(WeatherControl.class.getName())
                    .info(String.format("Requested forecasts for %s: not available!", place));
            throw ex;
            
        } catch (CityNotFoundException ex) {
            Logger.getLogger(WeatherControl.class.getName())
                    .info(String.format("Requested forecasts for %s: city not supported!", place));
            throw ex;
        }
        
        return weather;
        
    }
    
    /**
     * Periodically (every 3 hours) updates the weather forecasts for all events stored
     * in the database.
     * In case of bad weather this method sends the related notification to the creator
     * and the participating users. It also suggests to the creator an alternative date,
     * if available, with better weather conditions.
     * @throws CityNotFoundException if the specified event place is not supported
     * @throws UnavailableForecastException if there are no forecasts for the event date.
     */
    //@Schedule(hour = "*/3", persistent = false)
    @Schedule(second = "*/30", hour = "*", minute = "*", persistent = false)
    private void updateForecast() throws CityNotFoundException, UnavailableForecastException{
        List<Event> events = em.getAllEvents();
        String place;
        Calendar date;
        Weather weather = null;
        for(Event e : events){
            if(e.isOutdoor()){
                place = e.getPlace();
                date = e.getStartTime();
                try{
                    //Asks for weather forecast
                    weather = fp.getForecast(place, date);
                    Logger.getLogger(WeatherControl.class.getName())
                            .info(String.format("Requested forecasts for %s: %s", place, weather.toString().toLowerCase()));
                    //Checks if forecasts are bad for the event
                    if(weather.isWorseThan(e.getBadWeather())){
                        //Tries to find a date with better weather condition
                        Calendar newDate = fp.findAlternative(e);
                        if(newDate == null){
                            WeatherNotification not = buildNotification(e, weather);
                            if(isNotifiable(e.getCreator(), not)){
                                List<Users> user = new ArrayList<>();
                                user.add(e.getCreator());
                                nc.sendNotification(not, user);
                            }
                        }else{
                        //Notifies the creator of the event
                        AlternativeNotification alt = buildAlternative(e, weather, newDate);
                        if(isNotifiable(e.getCreator(), alt)){
                            List<Users> user = new ArrayList<>();
                            user.add(e.getCreator());
                            nc.sendNotification(alt, user);
                            }
                        }
                        //Notifies participants
                        WeatherNotification not = buildNotification(e, weather);
                        List<Users> users = new ArrayList<>();
                        for(Users u : em.getParticipants(e)){
                            if(isNotifiable(u, not))
                                users.add(u);
                        }
                        nc.sendNotification(not, users);
                    }
                } catch (UnavailableForecastException ex) {
                    Logger.getLogger(WeatherControl.class.getName())
                            .info(String.format("Requested forecasts for %s: not available!", place));
                    
                } catch (CityNotFoundException ex) {
                    Logger.getLogger(WeatherControl.class.getName())
                            .info(String.format("Requested forecasts for %s: city not supported!", place));
                }
            }
        }
    }    
    
    /**
     * Builds the notification to be sent to the event creator the notification
     * containing the weather forecast and the alternative scheduling date for
     * the specified event.
     * @param event
     * @param weather forecast
     * @param date with better weather conditions
     * @return the notification
     */
    private AlternativeNotification buildAlternative(Event event, Weather weather, Calendar date){
        AlternativeNotification not = new AlternativeNotification();
        not.setEvent(event);
        not.setBadWeather(weather);
        not.setAlternative(date);
        not.setSeen(false);
        return not;
    }
    
    /**
     * Builds the notification to be sent to all the participating users to a given
     * event, specifying the weather forecast.
     * @param event
     * @param weather forecast
     * @return the notification
     */
    private WeatherNotification buildNotification(Event event, Weather weather){
        WeatherNotification not = new WeatherNotification();
        not.setEvent(event);
        not.setBadWeather(weather);
        not.setSeen(false);
        return not;
    }
    
    /**
     * Checks if a notification with same informations has been already sent to
     * the user. In this case it updates the existing notification.
     * @param user
     * @param notification
     * @return true if the user was already notified, false otherwise
     */
    private boolean isNotifiable(Users user, Notification notification){
        if(notification instanceof AlternativeNotification){
            AlternativeNotification not = (AlternativeNotification) notification;
            
            //Finds the notifications received by the user
            List<Notification> list = um.getNotifications(user);
            for(Notification n : list){
                
                //Checks if the user has already received a notification with the same informations
                if(n instanceof AlternativeNotification){
                    AlternativeNotification old = (AlternativeNotification) n;
                    //If the informations are different, updates the existing notification
                    if(!not.equals(old)){
                        old.setBadWeather(not.getBadWeather());
                        old.setAlternative(not.getAlternative());
                        old.setSeen(false);
                        nm.update(old);
                        return false;
                    }
                    return false;
                }
            }
            return true;
        }
        
        if(notification instanceof WeatherNotification){
            WeatherNotification not = (WeatherNotification) notification;
            
            //Finds the notifications received by the user
            List<Notification> list = um.getNotifications(user);
            for(Notification n : list){
                
                //Checks if the user has already received a notification with the same informations
                if(n instanceof WeatherNotification){
                    WeatherNotification old = (WeatherNotification) n;
                    //If the informations are different, updates the existing notification
                    if(!not.equals(old)){
                        old.setBadWeather(not.getBadWeather());
                        old.setSeen(false);
                        nm.update(old);
                        return false;
                    }
                    return false;
                }
            }
            return true;
        }
        
        return false;
    }
    
}
